﻿// OraMaster.cs
// Aleksei Vassiljev
// {4DC17B88-5237-4e95-A60D-72988F554D4F}

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Win32;
using System.Diagnostics;
using System.Xml.Linq;
using System.Dynamic;

namespace OracleTools.Core
{
    public class OraEventArgs : EventArgs
    {
        public OraHome CurrentHome { get; private set; }
        public OraEventArgs(OraHome homeCurrent)
        {
            CurrentHome = homeCurrent;
        }
    }

    public class OraMaster
    {
        public event EventHandler<OraEventArgs> CurrentHomeChanged;

        /// <summary>
        /// List of all oracle homes
        /// </summary>
        public List<OraHome> Homes { get; private set; }

        private OraHome m_homeCurrent;
        public OraHome CurrentHome
        {
            get { return m_homeCurrent; }
            private set
            {
                m_homeCurrent = value;
                // Fire an event
                if (CurrentHomeChanged != null)
                    CurrentHomeChanged(this, new OraEventArgs(m_homeCurrent));
            }
        }

        public string LocaleNlsLang { get; private set; }

        private static OraMaster m_this = null;

        public static OraMaster GetInstance()
        {
            return m_this == null ? new OraMaster() : m_this;
        }
        /// <summary>
        /// ctor. Only create object, to get data - call LoadData() method afterward.
        /// </summary>
        private OraMaster()
        {
            Homes = new List<OraHome>();
            this.CurrentHome = null;
        }


        public void ChangeNlsLang(string lang)
        {
            EnvVar.SetValue("NLS_LANG", lang, EnvironmentVariableTarget.User);
            EnvVar.UpdateValue("NLS_LANG", lang, EnvironmentVariableTarget.Machine);
            this.LocaleNlsLang = lang;
        }

        /// <summary>
        /// Find all oracle homes installed.
        /// </summary>
        public void LoadData()
        {
            this.Homes.Clear();
            List<RegistryKey> lstKeyOraNodes = GetOracleBaseNodes();
            foreach(RegistryKey keyOraNode in lstKeyOraNodes)
            {
                List<OraHome> lstHomes = new List<OraHome>();
                bool bWow64 = (keyOraNode.ToString().ToLower().Contains("wow6432node"));

                IEnumerable<string> query = keyOraNode.GetSubKeyNames().
                    Where(c => c.StartsWith("KEY_") || c.StartsWith("HOME"));

                foreach (string sSubKey in query)
                {
                    OraHome home = OraHome.createInstance(keyOraNode.OpenSubKey(sSubKey));
                    if (home != null)
                        lstHomes.Add(home);
                }
                

                // Check if Inventory exists
                string sInstLoc = "";
                try
                {
                    sInstLoc = keyOraNode.GetValue("inst_loc", "") as string;
                }
                catch (Exception) { }
                if (!string.IsNullOrWhiteSpace(sInstLoc))
                {
                    string sInventoryFile = sInstLoc + @"\ContentsXML\inventory.xml";
                    if (File.Exists(sInventoryFile))
                    {
                        XDocument doc = XDocument.Load(sInventoryFile);
                        try
                        {
                            string sVersion = doc.Elements("INVENTORY").Elements("VERSION_INFO").Elements("SAVED_WITH").First().Value;
                            lstHomes.ForEach(home => home.Version = new OraVersion(sVersion));
                        }
                        catch (Exception) { }
                    }
                }


                // Get oracle homes versions from OracleMTSRecoveryService
                /*RegistryKey keyMTS = null;
                try
                {
                    keyMTS = keyOraNode.OpenSubKey(@"OracleMTSRecoveryService\Setup\All Versions", false);
                }
                catch (Exception) { }
                if (keyMTS != null)
                {
                    string[] arrMTSSubKeys = keyMTS.GetSubKeyNames();
                    foreach (string sMTSSubKey in arrMTSSubKeys)
                    {

                        RegistryKey keyMTSSub = keyMTS.OpenSubKey(sMTSSubKey, false);
                        string sHomeKey = keyMTSSub.GetValue("Home", "") as string;
                        string sValue = keyMTSSub.GetValue("Version", "") as string;
                        if ((sHomeKey != "") && (sValue != ""))
                        {
                            if(sHomeKey.ToLower().StartsWith("software\\"))
                                if (bWow64 && !sHomeKey.ToLower().Contains("wow6432node"))
                                    sHomeKey = sHomeKey.ToLower().Replace(@"software\oracle\", @"software\oracle\wow6432node");

                            foreach (OraHome home in lstHomes)
                                if (home.Key.ToLower() == sHomeKey.ToLower() || home.Path.ToLower() == sHomeKey.ToLower())
                                    if (!home.Version.HasValue)
                                    {
                                        home.Version = new OraVersion(sValue);
                                        break;
                                    }
                        }
                    }//foreach (string sMTSSubKey in arrMTSSubKeys)
                }
                */

                lstHomes.ForEach(home => this.Homes.Add(home));
            } // foreach(RegistryKey keyOraNode in lstKeyOraNodes)

            // Sort homes by name and path
            this.Homes.Sort((x, y) => (x.Name + x.Path).CompareTo(y.Name + y.Path));
            

            // Set current home property
            OraHome oraCurrent = DetermineCurrentOracleHome(lstKeyOraNodes);
            if (oraCurrent != null)
            {
                foreach (OraHome home in this.Homes)
                {
                    home.IsCurrent = (home == oraCurrent);
                    if (home.IsCurrent)
                        this.CurrentHome = home;
                }//foreach
            }//if (oraCurrent != null)



            // Determine locale NLS_LANG from environment variable
            // If environment variable is not defined - then read value from registry and set it as environment variable

            this.LocaleNlsLang = Environment.GetEnvironmentVariable("NLS_LANG", EnvironmentVariableTarget.User);
            if (string.IsNullOrWhiteSpace(this.LocaleNlsLang))
            {
                this.LocaleNlsLang = this.CurrentHome.NlsLang;
                EnvVar.SetValue("NLS_LANG", this.LocaleNlsLang, EnvironmentVariableTarget.User);
            }
        }

        public void SetAsCurrent(OraHome oraHome)
        {
            // Change values in SOFTWARE\ORACLE
            foreach (RegistryKey keyOraNode in GetOracleBaseNodes(true))
            {
                RegKeyEx reg = RegKeyEx.getInstance();


                reg.UpdateValue(keyOraNode, RegKeyEx.ORACLE_HOME, oraHome.Path);
                reg.UpdateValue(keyOraNode, RegKeyEx.ORACLE_HOME_NAME, oraHome.Name);
                reg.UpdateValue(keyOraNode, RegKeyEx.ORACLE_GROUP_NAME, oraHome.GroupName);
                reg.UpdateValue(keyOraNode, RegKeyEx.NLS_LANG, oraHome.NlsLang);
                reg.UpdateValue(keyOraNode, RegKeyEx.OLEDB, oraHome.OLEDB);
                reg.UpdateValue(keyOraNode, RegKeyEx.OO4O, oraHome.OO4O);
                reg.UpdateValue(keyOraNode, RegKeyEx.VOBHOME20, oraHome.Path);
                reg.UpdateValue(keyOraNode, "API", oraHome.SqlPath);

                // Change values in SOFTWARE\ORACLE\ALL_HOMES (exists in 9i)
                RegistryKey keyAllHomes = keyOraNode.OpenSubKey("ALL_HONES", true);
                reg.UpdateValue(keyAllHomes, "DEFAULT_HOME", oraHome.Name);

                // Change values in SOFTWARE\ORACLE\OO4O (exists in 9i)
                RegistryKey keyOO4O = keyOraNode.OpenSubKey("OO4O", true);
                reg.UpdateValue(keyOO4O, "OO4O_HOME", oraHome.OO4O);
                // TODO: also change path to oracleo.hlp file here

                // Change values in SOFTWARE\ORACLE\OracleMTSRecoveryService
                RegistryKey keyMTS = keyOraNode.OpenSubKey(@"OracleMTSRecoveryService\Setup\CurrentVersion", true);

                if (keyMTS != null)
                {
                    if (keyMTS.GetValue("Home").ToString().ToLower().StartsWith("software\\"))
                        reg.UpdateValue(keyMTS, "Home", oraHome.Key);
                    else
                        reg.UpdateValue(keyMTS, "Home", oraHome.Path);
                    reg.UpdateValue(keyMTS, "Version", oraHome.Version.ToString(), RegistryValueKind.String);
                }
            } //foreach (RegistryKey keyOraNode

            //UpdateRegistry(oraHome);
            UpdateLibraries(oraHome);

            // Update environment variable PATH
            UpdatePathEnvironmentVariable(oraHome, EnvironmentVariableTarget.User);
            UpdatePathEnvironmentVariable(oraHome, EnvironmentVariableTarget.Machine);

            EnvVar.SetValue("oracle_home", oraHome.Path, EnvironmentVariableTarget.User);
            if(Environment.GetEnvironmentVariable("oracle_home", EnvironmentVariableTarget.Machine) != null)
                EnvVar.SetValue("oracle_home", oraHome.Path, EnvironmentVariableTarget.Machine);

            // Set IsCurrent flag
            foreach (OraHome home in this.Homes)
                home.IsCurrent = (home.Equals(oraHome));

            this.CurrentHome = oraHome;
        }

        /// <summary>
        /// return HKLM\SOFTWARE\[Wow6432Node\]ORACLE registry keys if they contain sub-keys which start with KEY_ or HOME
        /// </summary>
        /// <param name="pbWritable"></param>
        /// <returns></returns>
        private List<RegistryKey> GetOracleBaseNodes(bool pbWritable = false)
        {
            List<RegistryKey> lstResult = new List<RegistryKey>();
            RegistryKey keyLM = Registry.LocalMachine;
            RegistryKey keyOracleNode1 = keyLM.OpenSubKey(@"SOFTWARE\ORACLE", pbWritable); // 32bit client on x86 or 64bit client on x64

            if (keyOracleNode1 != null)
            {
                if (keyOracleNode1.GetSubKeyNames().Where(c => c.StartsWith("KEY_") || c.StartsWith("HOME")).Any())
                    lstResult.Add(keyOracleNode1);
            }

            RegistryKey keyOracleNode2 = keyLM.OpenSubKey(@"SOFTWARE\Wow6432Node\ORACLE", pbWritable); // 32bit client on x64
            if (keyOracleNode2 != null)
            {
                if (keyOracleNode2.GetSubKeyNames().Where(c => c.StartsWith("KEY_") || c.StartsWith("HOME")).Any())
                    lstResult.Add(keyOracleNode2);
            }

            return lstResult;
        }

        /// <summary>
        /// Determine current oracle home by path in PATH environment variable
        /// </summary>
        /// <param name="keyOracleBases"></param>
        /// <returns></returns>
        private OraHome DetermineCurrentOracleHome(List<RegistryKey> keyOracleBases)
        {
            string sCurrentHomePath = "";
            OraHome oraCurrent = null;

            // If path to oracle home's bin folder is in path variable - then it's current oracle home

            for (int i = 0; i < 2; i++) // 2, because 2 targets - User and Machine
            {
                if (sCurrentHomePath != "") break;
                EnvironmentVariableTarget target = (i == 0 ? EnvironmentVariableTarget.User : EnvironmentVariableTarget.Machine);
                string sEnvPath = Environment.GetEnvironmentVariable("path", target);
                if (!string.IsNullOrWhiteSpace(sEnvPath))
                {
                    string[] arrPath = sEnvPath.ToLower().Split(';');

                    foreach (OraHome home in this.Homes)
                    {
                        foreach (string sPath in arrPath)
                        {
                            if (sPath == home.Path.ToLower() + "\\bin")
                            {
                                sCurrentHomePath = sPath;
                                oraCurrent = home;
                                break;
                            }
                        }
                        if (sCurrentHomePath != "") break;
                    }//foreach
                } // path not null
            }//for

            return oraCurrent;

        }

        private void UpdatePathEnvironmentVariable(OraHome homeNew, EnvironmentVariableTarget target)
        {
            string sEnvPath = Environment.GetEnvironmentVariable("path", target);
            if (string.IsNullOrWhiteSpace(sEnvPath)) return;

            List<string> lstPath = sEnvPath.Split(';').Distinct<string>().ToList<string>();

            // Remove paths to oracle_home and oracle_home\bin
            foreach (OraHome home in this.Homes)
                for (int i = 0; i < lstPath.Count; i++)
                    if ( (lstPath[i].ToLower() == home.BinPath.ToLower()) || (lstPath[i].ToLower() == home.Path.ToLower()) )
                        lstPath[i] = "";

            // Add paths of the current oracle home
            lstPath.Add(homeNew.Path);
            if (homeNew.BinPath != "") lstPath.Add(homeNew.BinPath);


            StringBuilder sb = new StringBuilder();
            lstPath.Where(s => s != "").ToList().ForEach(s => sb.Append(s + ";"));

            string sNewEnvPath = sb.ToString().Substring(0, sb.ToString().Length - 1);
            Environment.SetEnvironmentVariable("path", sNewEnvPath, target);
            LogWriter.LogLine("<{0}> path = {1}", target.ToString(), sNewEnvPath);
        }

        private void UpdateLibraries(OraHome home)
        {
            // Starting from 10g, there are a bunch of bat files in /bin directory,
            // which contain the list of commands need to change oracle home.

            List<string> lstCommands = new List<string>();
            string[] arrFiles = Directory.GetFiles(home.BinPath, "selecthome.bat*");
            foreach (string sFileName in arrFiles)
            {
                string[] arrLines = File.ReadAllLines(sFileName);

                for(int i=0; i<arrLines.Length; i++)
                {
                    string sLineLower = arrLines[i].ToLower();
                    if (sLineLower.Contains("%oracle_home%"))
                        arrLines[i] = sLineLower.Replace("%oracle_home%", home.Path);

                    if (sLineLower.StartsWith("if exist")) continue;
                    if (sLineLower.StartsWith(")")) continue;
                    lstCommands.Add(arrLines[i]);
                }
            }//foreach file


            lstCommands = lstCommands.Distinct<string>(StringComparer.OrdinalIgnoreCase).ToList<string>();

            // selecthome*.bat files may be missed
            // Therefore, add manually all commands which may be there

            if (home.OraOledbDllPath != string.Empty)
            {
                if (home.Version.Major <= 10)
                {
                    // I don't know why, but on my machine regsvr32 do not want to register OraOLEDB.dll from 9i
                    // Therefore, I have added such hardcode
                    int nInd = -1;
                    for(int i=0;i<lstCommands.Count;i++)
                        if (lstCommands[i].ToLower().Contains("regsvr32") &&
                            (lstCommands[i].ToLower().Contains(home.OraOledbDllPath.ToLower())))
                        {
                            nInd = i;
                            break;
                        }
                    if(nInd != -1)
                        lstCommands.RemoveAt(nInd);

                    UpdateTypelib(RegKeyEx.TYPELIB_ORAOLEDB_1_0_GUID, "1.0", home.OraOledbDllPath, home.BinPath);
                    UpdateClsid(RegKeyEx.CLSID_ORAOLEDB_GUID, home.OraOledbDllPath);
                    UpdateClsid(RegKeyEx.CLSID_ORAOLEDB_ERROR_LOOKUP_GUID, home.OraOledbDllPath);
                }
                else
                    lstCommands.Add(@"regsvr32 /s " + home.OraOledbDllPath);
            }
            if(home.OipDllPath != string.Empty) lstCommands.Add(@"regsvr32 /s " + home.OipDllPath);
            if(home.OraDcOcxPath != string.Empty) lstCommands.Add(@"regsvr32 /s " + home.OraDcOcxPath);
            
            if(File.Exists(home.BinPath + "\\oo4ocodewiz.dll"))
                lstCommands.Add(@"regsvr32 /s " + home.BinPath + "\\oo4ocodewiz.dll");
            if (File.Exists(home.BinPath + "\\odbtreeview.ocx"))
                lstCommands.Add(@"regsvr32 /s " + home.BinPath + "\\odbtreeview.ocx");
            if (File.Exists(home.BinPath + "\\oo4oaddin.dll"))
                lstCommands.Add(@"regsvr32 /s " + home.BinPath + "\\oo4oaddin.dll");
            
            if (Directory.Exists(home.Path + "\\oraconfig"))
            {
                lstCommands.Add(@"regsvr32 /s " + home.Path + "\\oraconfig\\oraconfigps" + home.Version.Major.ToString() + ".dll");
                lstCommands.Add(home.Path.ToLower() + "\\oraconfig\\oraconfig.exe -regserver");
            }


            IEnumerable<string> query = lstCommands.Distinct<string>(StringComparer.OrdinalIgnoreCase);
            foreach (string sLine in query)
            {
                string sExeLine = sLine;
                if(sExeLine.StartsWith("regsvr32") && !sExeLine.Contains("/s"))
                    sExeLine += " /s";

                ExecuteCommand(sExeLine);
            }


        }
        private void ExecuteCommand(string psCommand)
        {
            if (string.IsNullOrEmpty(psCommand)) return;
            LogWriter.LogLine(psCommand);
            int nPos = psCommand.IndexOf(" ");
            if (nPos > 0)
            {
                string sExe = psCommand.Substring(0, nPos);
                string sArg = psCommand.Substring(nPos + 1);
                ExecuteCommand(sExe, sArg);
            }
            else
                ExecuteCommand(psCommand, "");
        }
        private void ExecuteCommand(string psFileName, string psArguments)
        {
            try
            {
                System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo(psFileName, psArguments);
                info.CreateNoWindow = true;
                info.ErrorDialog = false;
                info.UseShellExecute = false;
                info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;

                if (!string.IsNullOrEmpty(psFileName))
                    System.Diagnostics.Process.Start(info);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        private void UpdateTypelibItem(RegistryKey keyRoot, string psBasePath, string psFileName, string psHelpDir)
        {
            RegistryKey key;
            RegKeyEx reg = RegKeyEx.getInstance();

            key = keyRoot.OpenSubKey(psBasePath + "\\0\\win32", true);
            reg.UpdateValue(key, "", psFileName);
            key = keyRoot.OpenSubKey(psBasePath + "\\HELPDIR", true);
            reg.UpdateValue(key, "", psHelpDir);
        }
        
        private void UpdateTypelib(string psGUID, string psVersion, string psFileName, string psHelpDir)
        {
            string sBasePath;

            // HKEY_CLASSES_ROOT\TypeLib\{GUID}
            sBasePath = string.Format(@"TypeLib\{0}\{1}", psGUID, psVersion);
            UpdateTypelibItem(Registry.ClassesRoot, sBasePath, psFileName, psHelpDir);

            // HKEY_LOCAL_MACHINE\SOFTWARE\Classes\TypeLib\{GUID}
            sBasePath = string.Format(@"SOFTWARE\Classes\TypeLib\{0}\{1}", psGUID, psVersion);
            UpdateTypelibItem(Registry.LocalMachine, sBasePath, psFileName, psHelpDir);

            if (Environment.Is64BitOperatingSystem)
            {
                // HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{GUID}
                sBasePath = string.Format(@"Wow6432Node\TypeLib\{0}\{1}", psGUID, psVersion);
                UpdateTypelibItem(Registry.ClassesRoot, sBasePath, psFileName, psHelpDir);

                // HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Wow6432Node\TypeLib\{GUID}
                sBasePath = string.Format(@"SOFTWARE\Classes\Wow6432Node\TypeLib\{0}\{1}", psGUID, psVersion);
                UpdateTypelibItem(Registry.LocalMachine, sBasePath, psFileName, psHelpDir);

                // HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Classes\TypeLib\{GUID}
                sBasePath = string.Format(@"SOFTWARE\Wow6432Node\Classes\TypeLib\{0}\{1}", psGUID, psVersion);
                UpdateTypelibItem(Registry.LocalMachine, sBasePath, psFileName, psHelpDir);
            }
        }

        private void UpdateClsid(string psGUID, string psFileName)
        {
            string sBasePath;
            RegKeyEx reg = RegKeyEx.getInstance();

            // HKEY_CLASSES_ROOT\CLSID\{GUID}\InProcServer32
            sBasePath = @"CLSID\{GUID}\InProcServer32".Replace("{GUID}", psGUID);
            reg.UpdateValue(Registry.ClassesRoot.OpenSubKey(sBasePath, true), "", psFileName);

            // HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{GUID}\InProcServer32
            sBasePath = @"SOFTWARE\Classes\CLSID\{GUID}\InProcServer32".Replace("{GUID}", psGUID);
            reg.UpdateValue(Registry.LocalMachine.OpenSubKey(sBasePath, true), "", psFileName);

            if (Environment.Is64BitOperatingSystem)
            {
                // HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{GUID}\InProcServer32
                sBasePath = @"Wow6432Node\CLSID\{GUID}\InProcServer32".Replace("{GUID}", psGUID);
                reg.UpdateValue(Registry.ClassesRoot.OpenSubKey(sBasePath, true), "", psFileName);

                // HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Wow6432Node\CLSID\{GUID}\InProcServer32
                sBasePath = @"SOFTWARE\Classes\Wow6432Node\CLSID\{GUID}\InProcServer32".Replace("{GUID}", psGUID);
                reg.UpdateValue(Registry.LocalMachine.OpenSubKey(sBasePath, true), "", psFileName);

                // HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Classes\CLSID\{GUID}\InProcServer32
                sBasePath = @"SOFTWARE\Wow6432Node\Classes\CLSID\{GUID}\InProcServer32".Replace("{GUID}", psGUID);
                reg.UpdateValue(Registry.LocalMachine.OpenSubKey(sBasePath, true), "", psFileName);
            }
        }

    }
}
